using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.AccessControl;
using System.Text.RegularExpressions;

namespace LightCAD.Three
{
    public interface IPropertyBinding
    {
        void getValue(IList array, int offset);
        void setValue(IList array, int offset);
        void bind();
        void unbind();
    }
    public partial class PropertyBinding
    {
        public delegate void GetValueFunc(IList array, int offset);
        public delegate void SetValueFunc(IList array, int offset);
        public class ParseResult
        {
            public string nodeName;
            public string objectName;
            public string objectIndex;
            public string propertyName; // required
            public string propertyIndex;
        }
        #region scope properties or methods
        private static string _RESERVED_CHARS_RE = "\\[\\]\\.:\\/";

        private static Regex _reservedRe = new Regex("[" + _RESERVED_CHARS_RE + "]");
        private static string _wordChar = "[^" + _RESERVED_CHARS_RE + "]";
        private static string _wordCharOrDot = "[^" + _RESERVED_CHARS_RE.replace("\\.", "") + "]";
        private static string _directoryRe = @"((?:WC+[\/:])*)".replace("WC", _wordChar);
        private static string _nodeRe = @"(WCOD+)?".replace("WCOD", _wordCharOrDot);
        private static string _objectRe = @"(?:\.(WC+)(?:\[(.+)\])?)?".replace("WC", _wordChar);
        private static string _propertyRe = @"\.(WC+)(?:\[(.+)\])?".replace("WC", _wordChar);
        private static Regex _trackRe = new Regex(""
        + '^'
        + _directoryRe
        + _nodeRe
        + _objectRe
        + _propertyRe
        + '$');
        private static ListEx<string> _supportedObjectNames = new ListEx<string> { "material", "materials", "bones", "map" };
        #endregion
        public class Composite : IPropertyBinding
        {

            #region Properties

            public AnimationObjectGroup _targetGroup;
            public ListEx<PropertyBinding> _bindings;

            #endregion

            #region constructor
            public Composite(AnimationObjectGroup targetGroup, string path, ParseResult optionalParsedPath)
            {
                var parsedPath = optionalParsedPath ?? PropertyBinding.parseTrackName(path);
                this._targetGroup = targetGroup;
                this._bindings = targetGroup.subscribe_(path, parsedPath);
            }
            #endregion

            #region methods
            public void getValue(IList array, int offset)
            {
                this.bind(); // bind all binding
                var firstValidIndex = this._targetGroup.nCachedObjects_;
                var binding = this._bindings[firstValidIndex];
                // and only call .getValue on the first
                if (binding != null) binding.getValueFunc(array, offset);
            }
            public void setValue(IList array, int offset)
            {
                var bindings = this._bindings;
                for (int i = this._targetGroup.nCachedObjects_, n = bindings.Length; i != n; ++i)
                {
                    bindings[i].setValueFunc.DynamicInvoke(array, offset);
                }
            }
            public void bind()
            {
                var bindings = this._bindings;
                for (int i = this._targetGroup.nCachedObjects_, n = bindings.Length; i != n; ++i)
                {
                    bindings[i].bind();
                }
            }
            public void unbind()
            {
                var bindings = this._bindings;
                for (int i = this._targetGroup.nCachedObjects_, n = bindings.Length; i != n; ++i)
                {
                    bindings[i].unbind();
                }
            }
            #endregion

        }
        public enum BindingType
        {
            Direct = 0,
            EntireArray = 1,
            ArrayElement = 2,
            HasFromToArray = 3
        };
        public enum Versioning
        {
            None = 0,
            NeedsUpdate = 1,
            MatrixWorldNeedsUpdate = 2
        };
        public ListEx<GetValueFunc> GetterByBindingType;
        public ListEx<ListEx<SetValueFunc>> SetterByBindingTypeAndVersioning;
    }
    public partial class PropertyBinding : IPropertyBinding
    {
        #region Properties
        //private static object Composite = Composite;
        public string path;
        public ParseResult parsedPath;
        public object node;
        public IAnimationObject rootNode;
        public GetValueFunc getValueFunc;
        public SetValueFunc setValueFunc;
        public object resolvedProperty;
        public int propertyIndex;
        public object targetObject;
        public string propertyName;
        #endregion

        #region constructor
        public PropertyBinding(IAnimationObject rootNode, string path, ParseResult parsedPath = null)
        {
            this.path = path;
            this.parsedPath = parsedPath ?? PropertyBinding.parseTrackName(path);
            this.node = PropertyBinding.findNode(rootNode, this.parsedPath.nodeName) ?? rootNode;
            this.rootNode = rootNode;
            // initial state of these methods that calls "bind"
            this.getValueFunc = this._getValue_unbound;
            this.setValueFunc = this._setValue_unbound;
            this.GetterByBindingType = new ListEx<GetValueFunc> {
                    new GetValueFunc( _getValue_direct),
                    new GetValueFunc(_getValue_array),
                    new GetValueFunc(_getValue_arrayElement),
                    new GetValueFunc(_getValue_toArray)
                };
            this.SetterByBindingTypeAndVersioning = new ListEx<ListEx<SetValueFunc>>
                {
                    new ListEx<SetValueFunc>
                    { 
                        // Direct
		                new SetValueFunc( _setValue_direct),
                        new SetValueFunc( _setValue_direct_setNeedsUpdate),
                        new SetValueFunc( _setValue_direct_setMatrixWorldNeedsUpdate)
                    },
                    new ListEx<SetValueFunc>
                    {
                        // EntireArray
		                new SetValueFunc(_setValue_array),
                        new SetValueFunc(_setValue_array_setNeedsUpdate),
                        new SetValueFunc(_setValue_array_setMatrixWorldNeedsUpdate)
                    },
                    new ListEx<SetValueFunc>
                    {
                        // ArrayElement
		                new SetValueFunc(_setValue_arrayElement),
                        new SetValueFunc(_setValue_arrayElement_setNeedsUpdate),
                        new SetValueFunc(_setValue_arrayElement_setMatrixWorldNeedsUpdate)
                    },
                    new ListEx<SetValueFunc>
                    {
                        // HasToFromArray
		                new SetValueFunc(_setValue_fromArray),
                        new SetValueFunc(_setValue_fromArray_setNeedsUpdate),
                        new SetValueFunc(_setValue_fromArray_setMatrixWorldNeedsUpdate)
                    }
                };
        }
        #endregion

        #region methods
        public static IPropertyBinding create(IAnimationObject root, string path, ParseResult parsedPath)
        {
            if (!(root is AnimationObjectGroup))
            {
                return new PropertyBinding(root, path, parsedPath);
            }
            else
            {
                return new Composite(root as AnimationObjectGroup, path, parsedPath);
            }
        }
        public static string sanitizeNodeName(string name)
        {
            return _reservedRe.Replace(new Regex(@"\s ", RegexOptions.Compiled).Replace(name, "_"), "");
        }
        public static ParseResult parseTrackName(string trackName)
        {
            var grps = _trackRe.Matches(trackName)[0].Groups;
            var matches = new ListEx<string>();
            for (int i = 0; i < grps.Count; i++)
            {
                var val = grps[i].Value;
                if (val == string.Empty)
                    matches[i] = null;
                else
                    matches[i] = val;
            }

            if (matches == null || matches.Count == 0)
            {
                throw new Error("PropertyBinding: Cannot parse trackName: " + trackName);
            }
            var results = new ParseResult
            {
                // directoryName: matches[ 1 ], // (tschw) currently unused
                nodeName = matches[2],
                objectName = matches[3],
                objectIndex = matches[4],
                propertyName = matches[5], // required
                propertyIndex = matches[6]

            };
            var lastDot = results.nodeName != null ? results.nodeName.LastIndexOf(".") : -1;
            if (lastDot != -1)
            {
                var objectName = results.nodeName.Substring(lastDot + 1);
                // Object names must be checked against an allowlist. Otherwise, there
                // is no way to parse "foo.bar.baz": "baz" must be a property, but
                // "bar" could be the objectName, or part of a nodeName (which can
                // include "." characters).
                if (_supportedObjectNames.IndexOf(objectName) != -1)
                {
                    results.nodeName = results.nodeName.Substring(0, lastDot);
                    results.objectName = objectName;
                }
            }
            if (results.propertyName == null || results.propertyName.Length == 0)
            {
                throw new Error("PropertyBinding: can not parse propertyName from trackName: " + trackName);
            }
            return results;
        }
        public static object findNode(object root, object nodeName)
        {
            if (nodeName == null || nodeName.ToString() == "" || nodeName.ToString() == "." || ((nodeName is int) && (int)nodeName == -1) ||
                            nodeName == root.GetFieldOrProperty("name") || nodeName == root.GetFieldOrProperty("uuid"))
            {
                return root;
            }
            if (root is SkinnedMesh)
            // search into skeleton bones.
            {
                var rootJsobj = (root as SkinnedMesh);
                if (rootJsobj.skeleton != null)
                {
                    var bone = rootJsobj.skeleton.getBoneByName(nodeName.ToString());
                    if (bone != null)
                    {
                        return bone;
                    }
                }
            }
            // search into node subtree.
            var rootObj3D = root as Object3D;
            if (rootObj3D != null && rootObj3D.children?.Count > 0)
            {
                var subTreeNode = searchNodeSubtree(rootObj3D.children, nodeName.ToString());
                if (subTreeNode != null)
                {
                    return subTreeNode;
                }
            }
            return null;
        }
        static Object3D searchNodeSubtree(ListEx<Object3D> children, string nodeName)
        {
            for (int i = 0; i < children.Length; i++)
            {
                var childNode = children[i];
                if (childNode.name == nodeName || childNode.uuid == nodeName)
                {
                    return childNode;
                }
                var result = searchNodeSubtree(childNode.children, nodeName);
                if (result != null) return result;
            }
            return null;
        }



        public void _getValue_unavailable(IList array, int offset)
        { }
        public void _setValue_unavailable(IList array, int offset)
        { }
        public void _getValue_direct(IList buffer, int offset)
        {
            buffer[offset] = this.targetObject.GetFieldOrProperty(this.propertyName);
        }
        public void _getValue_array(IList buffer, int offset)
        {
            var source = this.resolvedProperty as IList;
            for (int i = 0, n = source.Count; i != n; ++i)
            {
                buffer[offset++] = source[i];
            }
        }
        public void _getValue_arrayElement(IList buffer, int offset)
        {
            buffer[offset] = (this.resolvedProperty as IList)[this.propertyIndex];
        }
        public void _getValue_toArray(IList buffer, int offset)
        {
            this.resolvedProperty.InvokeMethod("toArray", buffer, offset);
        }
        public void _setValue_direct(IList buffer, int offset)
        {
            this.targetObject.SetFieldOrProperty(this.propertyName, buffer[offset]);
        }
        public void _setValue_direct_setNeedsUpdate(IList buffer, int offset)
        {
            this.targetObject.SetFieldOrProperty(this.propertyName, buffer[offset]);
            this.targetObject.SetFieldOrProperty("needsUpdate", true);
        }
        public void _setValue_direct_setMatrixWorldNeedsUpdate(IList buffer, int offset)
        {
            this.targetObject.SetFieldOrProperty(this.propertyName, buffer[offset]);
            this.targetObject.SetFieldOrProperty("matrixWorldNeedsUpdate", true);
        }
        public void _setValue_array(IList buffer, int offset)
        {
            var dest = this.resolvedProperty as IList;
            for (int i = 0, n = dest.Count; i != n; ++i)
            {
                dest[i] = buffer[offset++];
            }
        }
        public void _setValue_array_setNeedsUpdate(IList buffer, int offset)
        {
            var dest = this.resolvedProperty as IList;
            for (int i = 0, n = dest.Count; i != n; ++i)
            {
                dest[i] = buffer[offset++];
            }
            this.targetObject.SetFieldOrProperty("needsUpdate", true);

        }
        public void _setValue_array_setMatrixWorldNeedsUpdate(IList buffer, int offset)
        {
            var dest = this.resolvedProperty as IList;
            for (int i = 0, n = dest.Count; i != n; ++i)
            {
                dest[i] = buffer[offset++];
            }
            this.targetObject.SetFieldOrProperty("matrixWorldNeedsUpdate", true);
        }
        public void _setValue_arrayElement(IList buffer, int offset)
        {
            (this.resolvedProperty as IList)[this.propertyIndex] = buffer[offset];
        }
        public void _setValue_arrayElement_setNeedsUpdate(IList buffer, int offset)
        {
            (this.resolvedProperty as IList)[this.propertyIndex] = buffer[offset];
            this.targetObject.SetFieldOrProperty("needsUpdate", true);
        }
        public void _setValue_arrayElement_setMatrixWorldNeedsUpdate(IList buffer, int offset)
        {
            (this.resolvedProperty as IList)[this.propertyIndex] = buffer[offset];
            this.targetObject.SetFieldOrProperty("matrixWorldNeedsUpdate", true);
        }
        public void _setValue_fromArray(IList buffer, int offset)
        {
            this.resolvedProperty.InvokeMethod("fromArray", buffer, offset);
        }
        public void _setValue_fromArray_setNeedsUpdate(IList buffer, int offset)
        {
            this.resolvedProperty.InvokeMethod("fromArray", buffer, offset);
            this.targetObject.SetFieldOrProperty("needsUpdate", true);
        }
        public void _setValue_fromArray_setMatrixWorldNeedsUpdate(IList buffer, int offset)
        {
            this.resolvedProperty.InvokeMethod("fromArray", buffer, offset);
            this.targetObject.SetFieldOrProperty("matrixWorldNeedsUpdate", true);
        }
        public void _getValue_unbound(IList targetArray, int offset)
        {
            this.bind();
            this.getValueFunc.DynamicInvoke(targetArray, offset);
        }
        public void _setValue_unbound(IList sourceArray, int offset)
        {
            this.bind();
            this.setValueFunc.DynamicInvoke(sourceArray, offset);
        }

        public void getValue(IList array, int offset)
        {
            this.getValueFunc(array, offset);
        }

        public void setValue(IList array, int offset)
        {
            this.setValueFunc.DynamicInvoke(array, offset);
        }

        public void bind()
        {
            var targetObject = this.node;
            var parsedPath = this.parsedPath;
            var objectName = parsedPath.objectName;
            var propertyName = parsedPath.propertyName;
            var propertyIndex = parsedPath.propertyIndex;
            if (targetObject == null)
            {
                targetObject = PropertyBinding.findNode(this.rootNode, parsedPath.nodeName) ?? this.rootNode;
                this.node = targetObject;
            }
            // set fail state so we can just "return" on error
            this.getValueFunc = new GetValueFunc(this._getValue_unavailable);
            this.setValueFunc = new SetValueFunc(this._setValue_unavailable);
            // ensure there is a value node
            if (targetObject == null)
            {
                console.error("THREE.PropertyBinding: Trying to update node for track: " + this.path + " but it wasn\"t found.");
                return;
            }
            if (!string.IsNullOrEmpty(objectName))
            {
                var objectIndex = parsedPath.objectIndex;
                // special cases were we need to reach deeper into the hierarchy to get the face materials....
                switch (objectName)
                {
                    case "materials":
                        if (!targetObject.HasFieldOrProperty("material"))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to material as node does not have a material.", this);
                            return;
                        }
                        if (!targetObject.GetFieldOrProperty("material").HasFieldOrProperty("materials"))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.", this);
                            return;
                        }
                        targetObject = targetObject.GetFieldOrProperty("material").GetFieldOrProperty("material");
                        break;
                    case "bones":
                        if (!targetObject.HasFieldOrProperty("skeleton"))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to bones as node does not have a skeleton.", this);
                            return;
                        }
                        var skinedMesh = targetObject as SkinnedMesh;
                        // potential future optimization: skip this if propertyIndex is already an integer
                        // and convert the integer string to a true integer.
                        var bones = skinedMesh.skeleton.bones;
                        targetObject = skinedMesh.skeleton.bones;
                        // support resolving morphTarget names into indices.
                        for (int i = 0; i < bones.Length; i++)
                        {
                            if (bones[i].name == objectIndex)
                            {
                                objectIndex = i.ToString();
                                break;
                            }
                        }
                        break;
                    case "map":
                        if (targetObject.HasFieldOrProperty("map"))
                        {
                            targetObject = targetObject.GetFieldOrProperty("map");
                            break;
                        }
                        if (!targetObject.HasFieldOrProperty("material"))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to material as node does not have a material.", this);
                            return;
                        }
                        if (!(targetObject as Mesh).material.HasFieldOrProperty("map"))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to material.map as node.material does not have a map.", this);
                            return;
                        }
                        targetObject = (targetObject as Mesh).material.GetFieldOrProperty("map");
                        break;
                    case "material":

                    default:
                        if (!targetObject.HasFieldOrProperty(objectName))
                        {
                            console.error("THREE.PropertyBinding: Can not bind to objectName of node undefined.", this);
                            return;
                        }
                        targetObject = targetObject.GetFieldOrProperty(objectName);
                        break;
                }
                if (!string.IsNullOrEmpty(objectIndex))
                {
                    if (!targetObject.HasFieldOrProperty(objectIndex))
                    {
                        console.error("THREE.PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.", this, targetObject);
                        return;
                    }
                    targetObject = targetObject.GetFieldOrProperty(objectIndex);
                }
            }
            // resolve property
            var nodeProperty = targetObject.GetFieldOrProperty(propertyName);
            if (nodeProperty == null)
            {
                var nodeName = parsedPath.nodeName;
                console.error("THREE.PropertyBinding: Trying to update property for track: " + nodeName +
                    "." + propertyName + " but it wasn\"t found.", targetObject);
                return;
            }
            // determine versioning scheme
            var versioning = Convert.ToInt32(Versioning.None);
            this.targetObject = targetObject;
            if (targetObject.HasFieldOrProperty("needsUpdate"))
            { // material
                versioning = Convert.ToInt32(Versioning.NeedsUpdate);
            }
            else if (targetObject.HasFieldOrProperty("matrixWorldNeedsUpdate"))
            { // node transform
                versioning = Convert.ToInt32(Versioning.MatrixWorldNeedsUpdate);
            }
            // determine how the property gets bound
            var bindingType = Convert.ToInt32(BindingType.Direct);
            if (!string.IsNullOrEmpty(propertyIndex))
            {
                // access a sub element of the property array (only primitives are supported right now)
                if (propertyName == "morphTargetInfluences")
                {
                    // potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
                    // support resolving morphTarget names into indices.
                    if (!targetObject.HasFieldOrProperty("geometry"))
                    {
                        console.error("THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.", this);
                        return;
                    }
                    if (!targetObject.GetFieldOrProperty("geometry").HasFieldOrProperty("morphAttributes"))
                    {
                        console.error("THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.", this);
                        return;
                    }
                    if ((targetObject.GetFieldOrProperty("morphTargetDictionary") as JsObj<int>).ContainsKey(propertyIndex))
                    {
                        propertyIndex = (targetObject.GetFieldOrProperty("morphTargetDictionary") as JsObj<int>)[propertyIndex].ToString();
                    }
                }
                bindingType = Convert.ToInt32(BindingType.ArrayElement);
                this.resolvedProperty = nodeProperty;
                this.propertyIndex = int.Parse(propertyIndex);
            }
            else if (nodeProperty.HasMethod("fromArray") && nodeProperty.HasMethod("toArray"))
            {
                // must use copy for Object3D.Euler/Quaternion
                bindingType = Convert.ToInt32(BindingType.HasFromToArray);
                this.resolvedProperty = nodeProperty;
            }
            else if (nodeProperty is IList)
            {
                bindingType = Convert.ToInt32(BindingType.EntireArray);
                this.resolvedProperty = nodeProperty;
            }
            else
            {
                this.propertyName = propertyName;
            }
            // select getter / setter
            this.getValueFunc = this.GetterByBindingType[bindingType];
            this.setValueFunc = this.SetterByBindingTypeAndVersioning[bindingType][versioning];
        }
        public void unbind()
        {
            this.node = null;
            // back to the prototype version of getValue / setValue
            // note: avoiding to mutate the shape of "this" via "delete"
            this.getValueFunc = new GetValueFunc(this._getValue_unbound);
            this.setValueFunc = new SetValueFunc(this._setValue_unbound);
        }


        #endregion

    }
}
